﻿using System;
using System.Linq;
using LINQPad.Extensibility.DataContext;
using LINQPad.OpenAccess.Data;
using LINQPad.OpenAccess.Helpers;
using LINQPad.OpenAccess.Metadata;
using LINQPad.OpenAccess.Metadata.Reflection.Adapters;
using LINQPad.OpenAccess.Settings;
using Telerik.OpenAccess;
using Telerik.OpenAccess.Metadata;

namespace LINQPad.OpenAccess.Services
{
    /// <summary>
    /// <see cref="IDataContextInitializationService"/>.
    /// </summary>
    public class DataContextInitializationService : IDataContextInitializationService
    {
        #region Private

        #region - Vars

        private readonly IAssemblyParser _assemblyParser;
        private readonly IContextTypeInfoAdapter _contextTypeInfoAdapter;
        private readonly IConnectionStringBuilderFactory _connectionStringBuilderFactory;
        private readonly IBackendService _backendService;

        #endregion

        #endregion

        #region Protected

        #region - Functions

        /// <summary>
        /// Resolves the backend configuration.
        /// </summary>
        /// <param name="contextTypeInfo">The context type info.</param>
        /// <param name="driverSettings">The driver settings.</param>
        /// <param name="databaseInfo">The database info.</param>
        /// <returns>
        /// The backend configuration or <c>null</c>.
        /// </returns>
        /// <exception cref="System.InvalidOperationException">if the default backend configuration should be used, but the default providers doesn't match.</exception>
        protected BackendConfiguration ResolveBackendConfiguration( IContextTypeInfo contextTypeInfo, DriverSettings driverSettings, IDatabaseInfo databaseInfo )
        {
            BackendConfiguration backendConfiguration = null;
            if ( driverSettings.Backend != null && !driverSettings.Backend.IsDefault )
                backendConfiguration = this._backendService.GetBackendConfiguration( driverSettings.Backend, databaseInfo );
            if ( backendConfiguration == null && contextTypeInfo.DefaultBackendConfiguration != null )
            {
                if ( !String.IsNullOrWhiteSpace( databaseInfo.Provider )
                        && !databaseInfo.Provider.Equals( contextTypeInfo.DefaultBackendConfiguration.ProviderName ) )
                    throw new InvalidOperationException
                            (
                                String.Format
                                        (
                                            "The provider name of the default backend configuration ('{0}') differs from the manually configured one ('{1}') !"
                                            , contextTypeInfo.DefaultBackendConfiguration.ProviderName
                                            , databaseInfo.Provider
                                        )
                            );
                backendConfiguration = contextTypeInfo.DefaultBackendConfiguration;
            }
            if ( backendConfiguration == null && !String.IsNullOrWhiteSpace( databaseInfo.Provider ) )
                backendConfiguration = this._backendService.CreateDefaultConfigurationForProvider( databaseInfo.Provider );

            return backendConfiguration;
        }
        /// <summary>
        /// Resolves the connection string for the specified backend and provider.
        /// </summary>
        /// <param name="providerName">Name of the provider.</param>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="connectionSettings">The connection settings.</param>
        /// <returns>
        /// The connection string.
        /// </returns>
        /// <exception cref="System.InvalidOperationException">No database info available !</exception>
        protected string ResolveConnectionString( string providerName, IConnectionInfo connectionInfo, ConnectionSettings connectionSettings = null )
        {
            Guard.ArgumentNotNullOrEmpty( providerName, "providerName" );
            Guard.ArgumentNotNull( connectionInfo, "connectionInfo" );

            var builder = this._connectionStringBuilderFactory
                              .CreateConnectionStringBuilder( providerName, connectionSettings );
            if ( builder == null ) // fallback to default custom connection string
            {
                var databaseInfo = connectionInfo.DatabaseInfo;
                if ( databaseInfo == null )
                    throw new InvalidOperationException( "No database info available !" );

                var connectionString = databaseInfo.GetCxString( );
                if ( String.IsNullOrWhiteSpace( connectionString ) )
                    throw new InvalidOperationException( "No default connection string available !" );
                if ( !String.IsNullOrWhiteSpace( databaseInfo.Provider )
                        && !providerName.Equals( databaseInfo.Provider ) )
                    throw new InvalidOperationException
                            (
                                String.Format
                                        (
                                            "The specified provider '{0}' differs from the one of the default connection ('{1}') !"
                                            , providerName
                                            , connectionInfo.DatabaseInfo.Provider
                                        )
                            );
                //if ( databaseInfo.EncryptCustomCxString )
                //    return connectionInfo.Decrypt( databaseInfo.CustomCxString );
                return connectionString;
            }

            return builder.BuildConnectionString( providerName, connectionInfo, connectionSettings );
        }
        /// <summary>
        /// Resolves the metadata source for the specified context type.
        /// </summary>
        /// <param name="contextTypeInfo">The context type info.</param>
        /// <param name="metadataSourceSettings">The metadata source settings.</param>
        /// <returns>
        /// The metadata source or <c>null</c>.
        /// </returns>
        /// <exception cref="System.InvalidOperationException">If the default metadata source should be used, but is not available.</exception>
        protected MetadataSource ResolveMetadataSource( IContextTypeInfo contextTypeInfo, MetadataSourceSettings metadataSourceSettings = null )
        {
            Guard.ArgumentNotNull( contextTypeInfo, "contextTypeInfo" );
            MetadataSource metadataSource = null;

            if ( metadataSourceSettings != null )
            {
                var cmp = MetadataSourceInfoEqualityComparer.Default;
                metadataSource = this._assemblyParser
                                     .DiscoverMetadataSources( contextTypeInfo.ContextType.Assembly )
                                     .Where( i => cmp.Equals( i, metadataSourceSettings ) )
                                     .Select( i => i.Source )
                                     .FirstOrDefault( );
            }
            else if ( null == ( metadataSource = contextTypeInfo.DefaultMetadataSource ) )
                throw new InvalidOperationException( String.Format( "No metadata source specified and the context type '{0}' does not provide a default one !", contextTypeInfo.ContextType.FullName ) );

            return metadataSource;
        }

        #endregion

        #endregion

        #region Public

        #region - Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DataContextInitializationService" /> class.
        /// </summary>
        /// <param name="assemblyParser">The assembly parser.</param>
        /// <param name="backendService">The backend service.</param>
        /// <param name="connectionStringBuilderFactory">The connection string builder factory.</param>
        /// <param name="contextTypeInfoAdapter">The context type info adapter.</param>
        public DataContextInitializationService( IAssemblyParser assemblyParser
                                                    , IBackendService backendService
                                                    , IConnectionStringBuilderFactory connectionStringBuilderFactory
                                                    , IContextTypeInfoAdapter contextTypeInfoAdapter )
        {
            Guard.ArgumentNotNull( assemblyParser, "assemblyParser" );
            Guard.ArgumentNotNull( backendService, "backendService" );
            Guard.ArgumentNotNull( contextTypeInfoAdapter, "contextTypeInfoAdapter" );

            this._assemblyParser = assemblyParser;
            this._backendService = backendService;
            this._connectionStringBuilderFactory = connectionStringBuilderFactory;
            this._contextTypeInfoAdapter = contextTypeInfoAdapter;
        }

        #endregion

        #endregion

        #region IDataContextInitializationService Members

        /// <summary>
        /// Gets the metadata for the specified context.
        /// </summary>
        /// <param name="contextType">Type of the context.</param>
        /// <param name="metadataSourceSettings">The metadata source settings.</param>
        /// <returns>
        /// The context metadata.
        /// </returns>
        /// <exception cref="System.InvalidOperationException">If the context metadata could not be resolved.</exception>
        public MetadataContainer GetContextMetadata( Type contextType, MetadataSourceSettings metadataSourceSettings = null )
        {
            Guard.ArgumentNotNull( contextType, "contextType" );

            var contextTypeInfo = this._contextTypeInfoAdapter.Adapt( contextType );

            var metadataSource = this.ResolveMetadataSource( contextTypeInfo, metadataSourceSettings );
            if ( metadataSource == null )
                throw new InvalidOperationException( String.Format( "Failed to resolve metadata source for context type '{0}' !", contextType.FullName ) );
            return metadataSource.GetModel( );
        }
        /// <summary>
        /// Tries to get the constructor signature for the data context.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="initializationSettings">The initialization settings.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns>
        ///   <c>true</c> on success; otherwise,<c>false</c>.
        /// </returns>
        public bool TryGetContextConstructorSignature( IConnectionInfo connectionInfo, InitializationSettings initializationSettings, out ParameterDescriptor[] parameters )
        {
            if ( initializationSettings != null && initializationSettings.UseDefaultConstructor )
                parameters = new ParameterDescriptor[0];
            else
                parameters = new ParameterDescriptor[] 
                                {
                                    new ParameterDescriptor( "connection", typeof( string ).FullName ) 
                                    , new ParameterDescriptor( "backendConfiguration", typeof( BackendConfiguration ).FullName ) 
                                    , new ParameterDescriptor( "metadataSource", typeof( MetadataSource ).FullName ) 
                                };
            return true;
        }
        /// <summary>
        /// Gets the context initialization data.
        /// </summary>
        /// <param name="contextType">Type of the context.</param>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="driverSettings">The driver settings.</param>
        /// <returns>
        /// The context intialization data.
        /// </returns>
        /// <exception cref="System.InvalidOperationException"></exception>
        public DataContextInitializationData GetContextInitializationData( Type contextType, IConnectionInfo connectionInfo, DriverSettings driverSettings )
        {
            Guard.ArgumentNotNull( contextType, "contextType" );
            Guard.ArgumentNotNull( connectionInfo, "connectionInfo" );
            Guard.ArgumentNotNull( driverSettings, "driverSettings" );

            if ( driverSettings.Initialization != null && driverSettings.Initialization.UseDefaultConstructor ) // no initialization data needed
                return null;

            var contextTypeInfo = this._contextTypeInfoAdapter.Adapt( contextType );
            var databaseInfo = connectionInfo.DatabaseInfo;

            var backendConfiguration = this.ResolveBackendConfiguration( contextTypeInfo, driverSettings, databaseInfo );
            if ( backendConfiguration == null )
                throw new InvalidOperationException( String.Format( "Failed to resolve backend configuration for context type '{0}' !", contextType.FullName ) );

            var connectionString = this.ResolveConnectionString
                                            (
                                                backendConfiguration.ProviderName
                                                , connectionInfo
                                                , driverSettings.Connection
                                            );

            if ( String.IsNullOrWhiteSpace( connectionString ) )
                throw new InvalidOperationException( String.Format( "Failed to resolve connection string for context type '{0}' !", contextType.FullName ) );

            var metadataSource = this.ResolveMetadataSource( contextTypeInfo, driverSettings.MetadataSource );
            if ( metadataSource == null )
                throw new InvalidOperationException( String.Format( "Failed to resolve metadata source for context type '{0}' !", contextType.FullName ) );

            return new DataContextInitializationData
                        {
                            ConnectionString = connectionString,
                            BackendConfiguration = backendConfiguration,
                            MetadataSource = metadataSource
                        };
        }

        #endregion
    }
}
